#
# Copyright (c) 2012 Philip Jameson
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without 
# modification, are permitted provided that the following conditions are met:
#
#  - Redistributions of source code must retain the above copyright notice,
#    this list of conditions and the following disclaimer.
#  - Redistributions in binary form must reproduce the above copyright notice,
#    this list of conditions and the following disclaimer in the documentation 
#    and/or other materials provided with the distribution.
#  - Neither the name of the <ORGANIZATION> nor the names of its contributors 
#    may be used to endorse or promote products derived from this software 
#    without specific prior written permission.
#
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 
# THE POSSIBILITY OF SUCH DAMAGE.
#

import sys
import getopt
import ConfigParser
import os
import os.path
import socket
import logging

class Config:
	configFile = 'etc/apnsrpcd.conf'
	daemonize = True
	xmlListenAddress = '127.0.0.1'
	xmlListenPort = '8080'
	xmlCert = None
	xmlKey = None
	applications = {}
	pidFile = 'etc/apnsrpcd.pid'
	logFile = 'logs/apnsrpcd.log'
	logLevel = 'debug'
	logStderr = False
	logger = None
	allowedIps = ['127.0.0.1']
	requireAuth = False
	userInfo = [] #List of tuples of (user, password) that can connect to the xml rpc server

	"""Show command line usage"""
	@classmethod
	def usage(cls):
		print """HD APNS (Apple Push Notification Service) Server
%(line)-80s
%(help)-30s Shows this help
%(foreground)-30s Runs the server in the foreground
%(config)-30s Specifies the loation of the configuration file
%(sampleconfig)-30s Produces a sample configuration file
""" % { 'line' : '-' * 80, 'help' : '-h, --help', 'foreground' : '-f, --foreground', 'config' : '-c, --config=<config file>', 'sampleconfig' : '--sampleconfig' }

	"""Print out a basic configuration file"""
	@classmethod
	def sampleConfig(cls):
		print """HD APNS Sample Configuration File:
[apnsrpcd:main]
listen_ip = 0.0.0.0
listen_port = 8080
xmlserver_cert = etc/certs/cert.pem
xmlserver_key = etc/certs/key.pem
log_file = logs/apnsrpcd.log
log_level = debug
log_stderr = true
pid_file = etc/apnsrpcd.pid
allowed_ips = 127.0.0.1, 192.168.0.1
require_auth = true

[apnsrpcd:auth]
user1 = password
user2 = password2

[myapp]
cert = myapp.pem
dev = false
feedback_freq = 60
ca_cert = path_to_ca_cert.pem
"""

	"""Grab command line options. Return True if you should keep going, False if not (e.g. if help menu is shown)"""
	@classmethod
	def parseOptions(cls, argv):
		try:
			(options, args) = getopt.getopt(argv, "hfc:", ['help', 'config=', 'foreground', 'sampleconfig'])
		except getopt.GetoptError, e:
			print e
			print cls.usage()
			return False

		for option, value in options:
			if option in ('-h', '--help'):
				cls.usage()
				return False
			elif option == '--sampleconfig':
				cls.sampleConfig()
				return False
			elif option in ('-c', '--config'):
				cls.configFile = value
			elif option in ('-f', '--foreground'):
				cls.daemonize = False

		return True

	"""Parse the configuration file setup in parseOptions (or etc/apnsrpcd.conf). Returns True if no errors, else prints errors and returns False"""
	@classmethod
	def parseConfig(cls):
		if not os.path.exists(cls.configFile):
			print "Could not access configuration file '%s'" % cls.configFile
			return False

		parser = ConfigParser.ConfigParser()
		parsed = parser.read(cls.configFile)
		if len(parsed) == 0 or parsed[0] != cls.configFile:
			print "Could not parse configuration file '%s'" % cls.configFile
			return False

		if not parser.has_section('apnsrpcd:main'):
			print 'Configuration file must have a section named "main"'
			return False

		config = {}
		for section in parser.sections():
			config[section] = {}
			for option in parser.options(section):
				config[section][option] = parser.get(section, option).strip('"\'')

		error = False

		if 'daemonize' in config['apnsrpcd:main']:
			if config['apnsrpcd:main']['daemonize'].lower() in ('true', 'false'):
				if config['apnsrpcd:main']['daemonize'].lower() == 'true':
					cls.daemonize = True
				else:
					cls.daemonize = False
			else:
				print "'daemonize' must be either true, or false"
				error = True

		if 'xmlserver_cert' not in config['apnsrpcd:main']:
			print "'xmlserver_cert' must be set in the main section"
			error = True
		else:
			cls.xmlCert = config['apnsrpcd:main']['xmlserver_cert']

		if 'xmlserver_key' not in config['apnsrpcd:main']:
			print "'xmlserver_key' must be set in the main section"
			error = True
		else:
			cls.xmlKey = config['apnsrpcd:main']['xmlserver_key']

		if not os.path.exists(cls.xmlCert):
			print 'XMLRPC certificate file does not exist: %s' % cls.xmlCert
			error = True

		if not os.path.exists(cls.xmlKey):
			print 'XMLRPC key file does not exist: %s' % cls.xmlKey
			error = True


		if 'listen_ip' in config['apnsrpcd:main']:
			cls.xmlListenAddress = config['apnsrpcd:main']['listen_ip']

		if 'listen_port' in config['apnsrpcd:main']:
			try:
				port = int(config['apnsrpcd:main']['listen_port'])
				if port <= 0 or port > 65535:
					print "'listen_port' is not a valid port"
					error = True
				cls.xmlListenPort = port
			except ValueError:
				print "'listen_port' must be a valid port"
				error = True

		if 'allowed_ips' in config['apnsrpcd:main']:
			ips = [x.strip() for x in config['apnsrpcd:main']['allowed_ips'].split(',')]
			for ip in ips:
				try:
					socket.getaddrinfo(ip, None)
				except Exception, e:
					print "%s appears to be an invalid IP address: %s" % (ip, str(e))
					error = True
					break
			if error != True:
				cls.allowedIps = ips


		if 'log_file' in config['apnsrpcd:main']:
			cls.logFile = config['apnsrpcd:main']['log_file']

		if cls.logFile == None:
			print 'No PID file was specified'
			error = True
		else:
			if not os.access(os.path.dirname(cls.logFile), os.W_OK) and not os.access(cls.logFile, os.W_OK):
				print 'Could not write to log file %s or its parent directory (%s)' % (cls.logFile, os.path.dirname(cls.logFile))
				error = True

		if 'log_stderr' in config['apnsrpcd:main']:
			if config['apnsrpcd:main']['log_stderr'].lower() in ('true', 'false'):
				if config['apnsrpcd:main']['log_stderr'].lower() == 'true':
					cls.logStderr = True
				else:
					cls.logStderr = False

		if 'log_level' in config['apnsrpcd:main']:
			cls.logLevel = config['apnsrpcd:main']['log_level']
		cls.logLevel = cls.logLevel.lower()

		if cls.logLevel not in ['debug', 'info', 'critical', 'warning']:
			print "Invalid log level specified (%s). Must be one of 'debug', 'info', 'critical', 'warning'" % cls.logLevel
			error = True


		if 'pid_file' in config['apnsrpcd:main']:
			cls.pidFile = config['apnsrpcd:main']['pid_file']

		if cls.pidFile == None:
			print 'No PID file was specified'
			error = True
		else:
			if not os.access(os.path.dirname(cls.pidFile), os.W_OK) and not os.access(cls.pidFile, os.W_OK):
				print 'Could not write to PID file %s or its parent directory (%s)' % (cls.pidFile, os.path.dirname(cls.pidFile))
				error = True

		if 'require_auth' in config['apnsrpcd:main']:
			if config['apnsrpcd:main']['require_auth'].lower() in ('true', 'false'):
				if config['apnsrpcd:main']['require_auth'].lower() == 'true':
					cls.requireAuth = True
				else:
					cls.requireAuth = False

		if cls.requireAuth:
			if 'apnsrpcd:auth' not in config:
				print 'There is no [apnsrpcd:auth] section in the configuration, but authentication is required'
				error = True
			else:
				if len(config['apnsrpcd:auth']) == 0:
					print 'There were no users defined in [apnsrpcd:auth], but authentication is required'
					error = True
				else:
					for user, password in config['apnsrpcd:auth'].items():
						cls.userInfo.append( (user, password) )
						
		if error == False:
			for section, options in config.items():
				if section.startswith('apnsrpcd:'):
					continue

				if 'cert' not in options:
					print '"cert" must be set for application %s' % section
				elif not os.path.exists(options['cert']):
					print 'The certificate file for application %s (%s) does not exist, or is not readable' % (section, options['cert'])
					error = True

				if 'ca_cert' in options:
					if not os.path.exists(options['ca_cert']):
						print 'The CA certificate file for application %s (%s) does not exist, or is not readable' % (section, options['ca_cert'])
						error = True
				else:
					options['ca_cert'] = None

				if 'dev' not in options:
					print '"dev" must be set to true or false for application %s' % section
					error = True
				elif options['dev'].lower() != 'true' and options['dev'].lower() != 'false':
					print '"dev" must be set to true or false for application %s' % section
					error = True
				
				if 'feedback_freq' not in options:
					print '"feedback_freq" must be set for application %s' % section
					error = True
				else:
					try:
						options['feedback_freq'] = int(options['feedback_freq'])
						if options['feedback_freq'] < 10:
							print '"feedback_freq" for %s must be at least 10 seconds' % section
							error = True
					except ValueError:
						print '"feedback_freq" for %s must be a number of seconds to run check the feedback service'
						error = True

				if error == False:
					application = {'name' : section, 'apns_certificate' : options['cert'], 'dev' : True, 'feedback_freq' : options['feedback_freq'], 'apns_ca_certificate' : options['ca_cert']}
					if options['dev'].lower() == 'false':
						application['dev'] = False

					cls.applications[section] = application

				if error == True:
					break


		if error == False:
			#Setup the logger
			if cls.logLevel == 'debug':
				logLevel = logging.DEBUG
			elif cls.logLevel == 'info':
				logLevel = logging.INFO
			elif cls.logLevel == 'warning':
				logLevel = logging.WARNING
			elif cls.logLevel == 'error':
				logLevel = logging.ERROR
			elif cls.logLevel == 'critical':
				logLevel = logging.CRITICAL

			formatString = '%(asctime)s: %(name)s %(process)s - %(levelname)s: %(message)s'

			cls.logger = logging.getLogger('apnsrpcd')
			logging.basicConfig(filename = cls.logFile, level = logLevel, format = formatString)
			if cls.logStderr:
				cls.logger.addHandler(logging.StreamHandler(sys.stderr))


		if error == True:
			return False
		else:
			return True
